<?php
/**
 * PHP Template.
 */

define('AUTH_SYSTEM_USERS',3);

class auth extends Module {
  /** Базовая информация о пользователе **/
  var $user;
  /** Флаг необходимости сброса кеша настроек и данных о доступе для пользователя **/
  var $cache_reset
          /**  **/;
  var $pm_count=0;
  /** Конструктор. Если параметр $auto не false, сразу делаем аутентификацию. **/
  function auth($auto=true) {
    if ($auto) $this->do_auth();
  }

  /** Проверка корректности сессии или ключа пользователя и установка данных о нем в $user **/
  function do_auth() {
    global $app;
    $session_name=$app->get_opt('session_name'); // получаем имя идентификатора сессии
    session_name($session_name);
    if (isset($_REQUEST['authkey'])) { // сначала проверяем аутентификацию по ключу, она имеет наибольший приоритет
      list($uid,$key)=explode('-',$_REQUEST['authkey']);
      $rightkey=$this->gen_auth_key($uid); // генерируем правильный аутентификационый ключ для указанного uid и текущего (запрошенного) HURL и действия
      if ($uid>AUTH_SYSTEM_USERS && $_REQUEST['authkey']==$rightkey) $this->user=$this->load_user($uid); // если ключ правильный и пользователь не специальный, загружаем информацию о данном пользователе
      // ВАЖНО! Поскольку authkey должен срабатывать только для текущего действия, вместо использования функции set_user, которая создала бы сессию и длинный ключ, устанавливаем данные непосредственно в массив $user
      else $this->load_guest(); // если ключи не совпали, пользователь будет гостем
    }
    elseif (isset($_REQUEST[$session_name]) || isset($_COOKIE[$session_name])) { // если в запросе есть идентификатор сессии, используем данные оттуда
      $this->session(); // вызываем процедуру создания сессии, чтобы установить свой обработчик и проверить необходимость перезагрузки данных
      if (isset($_SESSION['auth'])) $this->user=$_SESSION['user']; // если сессия была инициализирована правильно (т.е. не произошло ее истечения или повреждения)
      else $this->load_guest();
    }
    elseif (isset($_COOKIE[$session_name.'_long'])) { // если в запросе есть cookie с длинным ключом, проверяем его корректность и устанавливаем данные пользователя
      list($uid,$key)=explode('-',$_COOKIE[$session_name.'_long']);
      $userdata = $this->load_user($uid);
      $rightkey=$this->gen_long_key($userdata,$session_name); // генерируем правильный длинный ключ для данного пользователя
      if ($_COOKIE[$session_name.'_long']==$rightkey) $this->set_user($userdata,true); // если ключ правильный, выставляем данные о пользователе и создаем его сессию
      // хотя в принципе длинного ключа достаточно для аутентификации, но взятие данных из сессии позволит выполнять аутентификацию быстрее, поэтому мы ее и создаем
      else $this->load_guest(); // если ключи не совпали, пользователь будет гостем
    }
    else $this->load_guest();
  }

  /** Проверка прав на выполнение запрошенного действия над заданным объектом.
   *
   * @param string $action Название действия. На данный момент предусмотрены следующие: view, read, post, newobject, edit, delete, moderate
   * @param mixed $object Объект, для которого проводится проверка. Если false, то проверка производится для того объекта, для которого сделан запрос.
   * @param int $user Пользователь, для которого производится проверка. Если false, проверяем текущего пользователя.
   */
  function check_access($action,$object=false,$user=false) {
    // TODO: доделать нормальную проверку прав, сейчас упрощенная
    if ($action=='read' || $action=='view') return true;
    elseif ($action=='post' || $action=='newobject') return !$this->is_guest();
    else return $this->is_admin();
  }


  /** Проверка пароля пользователя без входа в систему.
   *
   * @global Application $app
   * @param int $uid Идентификатор пользователя
   * @param string $password Пароль в открытом виде
   * @return boolean TRUE, если пароль верен
   */
  function check_password($uid,$password) {
    global $app;
    $userdata=$this->load_user($uid);
    $crypted_password=$this->crypt_password($password,$userdata['pass_crypt']);
    if ($userdata['password']!=$crypted_password) return false; //если шифрованный пароль не совпал с хранящимся в базе, выходим из процедуры
    else return true;
  }

  /** Осуществление входа на форум по логину/паролю, а не по сессии или ключу **/
  function do_login($login,$password,$long=false) {
    global $app;

    $userdata=$this->load_user(0,$login); // загружаем данные о пользователе по его имени, а не по uid
    $crypted_password=$this->crypt_password($password,$userdata['pass_crypt']);
    if ($userdata['password']!=$crypted_password) return false; //если шифрованный пароль не совпал с хранящимся в базе, выходим из процедуры
    $this->set_user($userdata,$long);
    return true;
  }

  /** Выход с форума **/
  function do_logout() {
    global $app;
    $_SESSION['auth']=false;
    session_destroy();
    $session_name=$app->get_opt('session_name');
    if (isset($_COOKIE[$session_name.'_long'])) setcookie($session_name.'_long','',1);
    $this->load_guest(); //
  }

  function load_guest() {
    global $app;
    $session_name=$app->get_opt('session_name');
    if (isset($_COOKIE[$session_name.'_long'])) setcookie($session_name.'_long','',1);
    $this->user=$this->load_user(1);
    if (isset($_SESSION['auth'])) $_SESSION['user']=$this->user;
  }

  /** Загрузка информации о пользователе из базы данных **/
  //TODO: заменить потом  * в запросе на список полей
  function load_user($uid,$name=false) {
    global $app;
    if ($name) $condition='login="'.$app->slashes($name).'"';
    else $condition='id='.intval($uid);
    $sql = 'SELECT * FROM '.DB_prefix.'user WHERE '.$condition;
    $userdata = $app->db->select_row($sql);
    return $userdata;
  }

  /** Установка данных о пользователе в $user, а также создание сессии и длинного ключа аутентификации **/
  function set_user($userdata,$long=false) {
    global $app;
    $this->session(); // создаем сессию, если это необходимо
    if ($long) {
      $key=$this->gen_long_key($userdata);
      $period=time()+$app->get_opt('long_key_time')*24*60*60;
      setcookie($app->get_opt('session_name').'_long',$key,$period);
    }
    unset($userdata['password']);
    $_SESSION['auth']=1; // признак того, что сессия корректно инициализирована
    $_SESSION['user']=$userdata; // устанавливаем данные о пользователе
    $_SESSION['starttime']=time(); // фиксируем время начала сессии
    $this->user=$userdata;
  }

  /**     **/
  function crypt_password($password,$method) {
    if ($method==1) return md5($password);
    else return $password;
  }

  /** Генерация аутентификационного ключа с привязкой к URL и действию **/
  function gen_auth_key($uid=false,$action=false,$url=false) {
    global $app;
    if ($uid<=AUTH_SYSTEM_USERS) return '1-='; // для специальных пользователей аутентификация по ключу невозможна
    if (!$url) $url=$app->url;
    if (!$action) $action=$app->action;
    if (!$uid) $uid=$this->user['id'];
    $userdata = $this->load_user($uid);
    return $uid.'-'.md5($userdata['id'].$action.$userdata['rnd'].$url.$userdata['password'].$userdata['pass_crypt'].$userdata['email']);
  }

  /** Генерация ключа для долгосрочной идентификации **/
  function gen_long_key($userdata,$session_name=false) {
    global $app;
    if (!$session_name) $session_name=$app->get_opt('session_name');
    // TODO: доделать добавку очищенного User Agent
    return $userdata['id'].'-'.md5($userdata['id'].$userdata['password'].$userdata['rnd'].$userdata['pass_crypt'].$session_name);
  }

  /** Принудительное создание сессии **/
  function session() {
    global $app;

    // устанавливаем собственный обработчик сессий, чтобы хранить их в базе данных, а не в файлах
    session_set_save_handler(array($this,'session_open'), array($this,'session_close'), array($this,'session_read'), array($this,'session_write'), array($this,'session_destroy'), array($this,'session_clear'));
    $session_name=$app->get_opt('session_name'); // получаем имя идентификатора сессии
    session_name($session_name);
    $result=true;
    if (session_id()==false) $result=session_start();
    if (!$result) {
      trigger_error('Ошибка при создании сессии!',E_USER_WARNING);
    }

    if ($result && $this->cache_reset) { // если обнаружилось, что нужно сбросить кеш данных пользователя
      if (isset($_SESSION['cache'])) unset($_SESSION['cache']); // сбрасываем настройки пользователя
      $sql = 'UPDATE '.DB_prefix.'session SET refresh=0 WHERE uid='.intval($this->get_uid()); // обнуляем флаг устаревания сесии в БД, так как кеш уже сброшен
      $this->cache_reset=false;
      $this->set_user($this->load_user($this->user['id'])); // перезагружаем базовые данные о пользователе
      $app->db->query($sql);
    }
    return $result;
  }

  /** Получение идентификатора текущего пользователя **/
  function get_uid() {
    return $this->user['id'];
  }

  /** Возвращает имя (логин) пользователя  * **/
  function get_name($uid=false) {
    if ($uid==false) return $this->user['login'];
    // TODO: предусмотреть возврат имен пользователей по массиву uid
  }

  /** Проверка на то, что текущий пользователь является гостем **/
  function is_guest() {
    return ($this->user['id']==1);
  }

  /** Простая проверка наличия админских прав (в частности, доступа в АЦ)
   * TODO: доделать **/
  function is_admin() {
    return ($this->user['admin']==1);
  }

  /** Проверка на то, что пользователь активирован **/
  function is_activated() {
    return $this->user['active']==1;
  }

  /** Проверка на то, что пользователь забанен **/
  function is_banned() {
    return $this->user['banned']==0;
  }

  /** Получение списка разделов, для которых запрошенное действие запрещено.
   * Для текущего пользователя список может кешироваться в $_SESSION['forbidden'][$action].
   *  **/
  function forbidden_list($action,$user=false) {
    // TODO: доделать получение списка запрещенных объектов, а также его кеширование в сессии
    $result=array();
    return $result;
  }


  /** Функция инициализации сессии. При хранении сессии в БД не используется, но должна существовать и возвращать true. **/
  function session_open($save_path,$name) {
    return true;
  }

  /** Функция закрытия сессии. При хранении сессии в БД не используется, но должна существовать и возвращать true. **/
  function session_close() {
    return true;
  }

  /** Извлечение данных сессии из таблицы. Кроме собственно сессии извлекаются флаг необходимости сброса кеша и количество непрочитанных ЛС для данного пользователя.
   *  **/
  function session_read($sid) {
    global $app;
    $sql = 'SELECT refresh, pm_count, data FROM '.DB_prefix.'session WHERE id="'.$app->slashes($sid).'"';
    $data = $app->db->select_row($sql);
    if (!$data) return '';
    if ($data['refresh']==1) $this->cache_reset=true;
    $this->pm_count=$data['pm_count'];
    return $data['data'];
  }

  /** Запись данных сессии в таблицу. Кол-во непрочитанных ЛС берется из переменной pm_count, соответственно, модуль ЛС должен его выставлять (?)
   *
   * @global Application $app
   * @param string $sid Идентфиикатор сессии
   * @param string $string_data Данные сессии
   * @return boolean TRUE, если операция прошла успешно.
   * Параметр refresh в данной функции не изменяется, он сбрасывается в session сразу же после чтения данных сессии и обнуления кеша.
   */
  function session_write($sid,$string_data) {
    global $app;
    $data['data']=$string_data;
    $data['uid']=$this->get_uid();
    $data['id']=$sid;
    $data['lastmod']=time();
    $data['pm_count']=$this->pm_count;
    $result=$app->db->replace(DB_prefix.'session',$data,'id="'.$app->slashes($sid).'"');
    return $result;
  }

  /** Закрытие сессии и удаление ее данных из таблицы
   *
   * @global Application $app
   * @param string $sid Идентфиикатор сессии
   * @return bool TRUE, если операция прошла успешно.
   */
  function session_destroy($sid) {
    global $app;
    $sql = 'DELETE FROM '.DB_prefix.'session WHERE id="'.$app->slashes($sid).'"';
    $result=$app->db->query($sql);
    return $result;
  }

  /** Удаление сессий, с момента последнего обновления которых прошло больше определенного времени.
   *
   * @global Application $app
   * @param int $period Период задается в секундах (для совместимости со стандартными функциями PHP), в отличие от многих процедур clear в другим модулях!
   * @return bool TRUE, если операция прошла успешно.
   */
  function session_clear($period=3600) {
    global $app;
    $lasttime = time()-$period;
    $sql = 'DELETE FROM '.DB_prefix.'session WHERE lastmod<="'.intval($lasttime);
    $result=$app->db->query($sql);
    return $result;
  }

  /** Получение идентификатора пользователя по его логин или каноническому имени. Может использоваться так же для проверки существования пользователя.
   *
   * @global Application $app
   * @param string $login Логин пользователя для проверки
   * @return int Идентификатор пользователя, если он найден, или false, если пользователя с таким логином не существует.
   */
  function get_uid_by_name($login) {
    global $app;
    $sql = 'SELECT id FROM '.DB_prefix.'user WHERE (login="'.$app->slashes($login).'" OR canonical="'.$app->slashes($login).'")';
    return $app->db->select_int($sql);
  }

  /** Получение идентификатора пользователя по его Email. Может использоваться так же для проверки, что EMail уже использован.
   *
   * @global Application $app
   * @param string $email EMail для проверки
   * @return int Идентфиикатор пользователя, если он найден, или false, если пользователя с таким EMail не существует
   */
  function get_uid_by_email($email) {
    global $app;
    $sql = 'SELECT id FROM '.DB_prefix.'user WHERE email="'.$app->db->slashes($email).'"';
    return $app->db->select_int($sql);
  }

  /** Создание основной записи пользовательского профиля
   *
   * @global Application $app
   * @param array $data Базовые данные пользователя
   * @return int Номер (идентификатор) пользователя
   **/
  function register_profile($data) {
    global $app;
    $data['password']=$this->crypt_password($data['password'], $data['pass_crypt']);
    // TODO: подумать, где хранить дату регистрации и IP регистрации, возможно, в отдельной таблице
    $data['reg_ip']=ip2long($_SERVER['REMOTE_ADDR']); // фиксируем IP-адрес регистрации
    $data['rnd']=mt_rand();
    $data['id']=$app->db->insert(DB_prefix.'user',$data);
    return $data['id'];
  }

  /** Внесение изменений в основную запись профиля пользователя
   *
   * @global Application $app
   * @param array $data Измененные данные о пользователе. Те поля, которые не требуется изменять, должны быть не установлены. Обязательно должно присутствовать поле id.
   * @return boolean
   */
  function update_profile($data) {
    global $app;
    if (isset($data['password'])) $data['password']=$this->crypt_password($data['password'], $data['pass_crypt']);
    $result=$app->db->update(DB_prefix.'user',$data,'id='.intval($data['id']));
    if ($data['id']==$this->user['id']) { // если обновляем профиль текущего пользователя, то обновляем и локальные данные
      $newdata=$this->load_user($data['id']);
      $this->set_user($newdata);
    }
    return $result;
  }

  /** Удаление основной записи пользовательского профиля и сессий пользователя, если они имеются
   *
   * @global Application $app
   * @param int $id
   *
   * TODO: доделать возможность передачи массива идентификаторов.
   */
  function delete_profile($id) {
    global $app;
    $sql = 'DELETE FROM '.DB_prefix.'user WHERE id='.intval($id);
    $result=$app->db->query($sql);
    $sql = 'DELETE FROM '.DB_prefix.'session WHERE uid='.intval($id);
    $app->db->query($sql);
    return $result;
  }

  /** Установка признака устаревания данных в кеше сессии пользователя.
   *  При следующем действии пользователя произойдет очистка этих данных.
   *
   * @global Application $app
   * @param mixed $ids Массив идентификаторов пользователя или строка с идентификаторами, разделенными запятой.
   */
  function reset_cache($ids) {
    global $app;
    $sql = 'UPDATE '.DB_prefix.'session SET refresh=1 WHERE '.$app->db->array_to_sql($ids,'uid');
    return $app->db->query($sql);
  }
}
?>
